# -*- coding: utf-8 -*-

"""
@author: bw
@contact: bw@handsdriver.net
"""

__all__ = ('safecall', 'safeattr', 'safecontext', 'safeeval', 'safecode')

import sys
import ast
import contextlib
import inspect

from . import imports
from . import reflect
from . import string

#failure = imports.lazyModule('eof.failure')
log = imports.lazyModule('eof.log')


STUB = object()


def safecall(func, *args, **kwargs):
	try:
		if type(func) in (tuple, list) and func:
			func, *tail = func
			while tail:
				name, *tail = tail
				if '.' in name:
					name, *head = name.split('.', 1)
					tail = head + list(tail)
				func = getattr(func, name)
		elif isinstance(func, str):
			func = reflect.namedAny(func)
		return func(*args, **kwargs)
	except Exception as exc:
		log.error('[{etype}] {evalue}', etype=type(exc).__name__, evalue=string.safestr(exc))


def safeattr(obj, name, default=STUB, logexc=True):
	try:
		return getattr(obj, name)
	except Exception as exc:
		if logexc:
			log.error('Failure get attr "{name}" of {obj!r}: {exc!s}', name=name, obj=obj, exc=exc)
		return exc if default is STUB else default


@contextlib.contextmanager
def safecontext(failmsg='Unhandled Error', shortout=False):
	result = safecontext_result()
	try:
		yield result
	except Exception as exc:
		result.setFailure()
		if shortout:
			log.error('{msg}: [{etype}] {evalue}', msg=failmsg, etype=type(exc).__name__, evalue=exc)
		else:
			log.exception(failmsg)
	except:
		result.setFailure()
		raise
	else:
		result.setSuccess()


class safecontext_result:
	__slots__ = 'failure'

	__failure__ = STUB

	def __init__(self):
		self.failure = None

	def __bool__(self):
		return self.failure is None

	def setFailure(self, fail=None):
		failure = self.__failure__

		if failure is STUB:
			try:
				import eof.failure as failure
			except ImportError:
				failure = None
			type(self).__failure__ = failure

		if failure is not None:
			if type(fail) is failure.Failure:
				self.failure = fail
			elif isinstance(fail, BaseException):
				self.failure = failure.Failure(fail)
			elif inspect.isclass(fail) and issubclass(fail, BaseException):
				self.failure = failure.Failure(fail())
			elif fail is not None:
				self.failure = failure.Failure(Exception(fail))
			else:
				try:
					self.failure = failure.Failure()
				except failure.NoCurrentExceptionError:
					self.failure = failure.Failure(Exception('Unknown error'))
		else:
			if isinstance(fail, BaseException):
				self.failure = fail
			elif inspect.isclass(fail) and issubclass(fail, BaseException):
				self.failure = fail()
			elif fail is not None:
				self.failure = Exception(fail)
			else:
				_, evalue, _ = sys.exc_info()
				if evalue is not None:
					self.failure = evalue
				else:
					self.failure = Exception('Unknown error')

	def setSuccess(self):
		pass


def safeeval(expression, context={}, safenodes=None, addnodes=None):
	"""C-style simplified wrapper, eval() replacement."""
	code   = safecode(expression, safenodes, addnodes, 'eval')
	result = eval(code, context.copy())
	return result


def safecode(expression, safenodes=None, addnodes=None, mode='eval'):
	node = ast.parse(str(expression), '<safecode>', mode)
	SafeAST(safenodes, addnodes).visit(node)
	return compile(node, '<safecode>', mode)


class SafeAST(ast.NodeVisitor):
	allowed = {}

	def __init__(self, safenodes=None, addnodes=None):
		if safenodes is not None:
			self.allowed = safenodes
		else:
			# 123, 'asdf'
			values = ['Num', 'Str']
			# any expression
			expression = ['Expression']
			# == ...
			compare = ['Compare', 'Eq', 'NotEq', 'Gt', 'GtE', 'Lt', 'LtE']
			# variable name
			variables = ['Name', 'Load']
			binop = ['BinOp']
			arithmetics = ['Add', 'Sub', 'Mult', 'Div', 'Pow']
			subscript = ['Subscript', 'Index']  # person['name']
			boolop = ['BoolOp', 'And', 'Or', 'UnaryOp', 'Not']  # True and True
			inop = ["In"]  # "aaa" in i['list']
			ifop = ["IfExp"] # for if expressions, like: expr1 if expr2 else expr3
			nameconst = ["NameConstant"] # for True and False constants

			self.allowed = expression + values + compare + variables + binop + \
				arithmetics + subscript + boolop + inop + ifop + nameconst

			self.allowed += ['Call', 'Attribute']  # [bw] ???

		if addnodes is not None:
			self.allowed = self.allowed + addnodes

	def generic_visit(self, node):
		"""Check node, rais exception is node is not in whitelist."""
		if type(node).__name__ not in self.allowed:
			raise ValueError('Operaton type {} is not allowed'.format(type(node).__name__))
		ast.NodeVisitor.generic_visit(self, node)
